﻿
using System;
using System.Net;
using System.Net.Sockets;

namespace TcpAsyncServer
{
    abstract class SockDisposableBase : IDisposable
    {
        private bool m_disposed = false;

        public abstract Socket OwnedSocket { get; }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool isDisposing)
        {
            if (m_disposed)
                return;

            try
            {
                // all the sockets connecting to the aliving clients will be closed 
                // automatically when the program exits
                if (isDisposing)
                    OwnedSocket.Close();
            }
            finally
            {
                m_disposed = true;
            }
        }
    }

    sealed class EchoEvtArgs : EventArgs
    {
        private Socket m_recvSocket;
        private byte[] m_recvBuffer;
        private int m_recvLength;

        public EchoEvtArgs(Socket recvSocket, byte[] recvBuffer, int recvLength)
        {
            m_recvSocket = recvSocket;
            m_recvBuffer = recvBuffer;
            m_recvLength = recvLength;
        }

        public Socket RecvSocket { get { return m_recvSocket; } }
        public byte[] RecvBuffer { get { return m_recvBuffer; } }
        public int RecvLength { get { return m_recvLength; } }
    }

    /// <summary>
    /// wrapper of the socket connecting to the clients
    /// </summary>
    sealed class EchoSocket : SockDisposableBase
    {
        //===============================================//
        #region [ member variables ]

        private Socket m_socket;

        // only one buffer is used for both receiving and sending
        // this is because that we will not launch asynchronous receiving and sending
        // even we can copy user-defined buffer to avoid race condition, race condition to socket's own
        // buffer still exists, so receiving and sending process are sequenced here
        private byte[] m_buffer = new byte[1024];

        public event EventHandler<EchoEvtArgs> ReceivedEvtHandler;
        public event EventHandler<EchoEvtArgs> ClosedEvtHandler;

        #endregion

        //===============================================//
        #region [ constructor and destructor ]

        public EchoSocket(Socket clientSocket)
        {
            m_socket = clientSocket;
        }

        ~EchoSocket()
        {
            Dispose(false);
        }

        #endregion

        //===============================================//
        #region [ property ]

        public override Socket OwnedSocket
        {
            get { return m_socket; }
        }

        #endregion

        //===============================================//
        #region [ public methods ]

        public void Read()
        {
            m_socket.BeginReceive(m_buffer, 0, m_buffer.Length, SocketFlags.None, OnReceivedCallback, null);
        }

        #endregion

        //===============================================//
        #region [ private methods ]

        private void OnReceivedCallback(IAsyncResult asyncResult)
        {
            int recvLength = 0;
            try
            {
                recvLength = m_socket.EndReceive(asyncResult);
            }
            catch (SocketException ex)
            {
                if (ex.SocketErrorCode == SocketError.ConnectionReset)
                    recvLength = 0; // indicating remote terminal is closed
                else
                {
                    Dispose();
                    throw ex;
                }
            }
            
            if (recvLength > 0)
            {
                FireReceiveEvent(recvLength);

                // must first send synchronously, and then receive asynchronously, otherwise will result in
                // simultaneous sending to one socket, which results in race condition to socket's sending buffer
                // after "Send", the content in "m_buffer" has been totally copied into socket's own sending buffer
                // then "m_buffer" can be re-used for receiving, then no race condition
                m_socket.Send(m_buffer, 0, recvLength, SocketFlags.None);
                Read();
            }
            else
            {
                FireCloseEvent();
                Dispose();
            }
        }

        private void FireReceiveEvent(int recvLength)
        {
            if (ReceivedEvtHandler != null)
                ReceivedEvtHandler(this,new EchoEvtArgs(m_socket,m_buffer,recvLength));
        }

        private void FireCloseEvent()
        {
            if (ClosedEvtHandler != null)
                ClosedEvtHandler(this,new EchoEvtArgs(m_socket,null,0));
        }

        #endregion
    }

    sealed class AcceptEvtArgs : EventArgs
    {
        private EchoSocket m_echoSock;

        public AcceptEvtArgs(EchoSocket echosock)
        {
            m_echoSock = echosock;
        }

        public EchoSocket EchoSock
        {
            get { return m_echoSock; }
        }
    }

    sealed class TcpServer : SockDisposableBase
    {
        //===============================================//
        /// <summary>
        /// pay attention that, in this server, I only keep the listening socket as the member
        /// variable, and I don't keep a list of all the connected clientss
        /// </summary>
        #region [ member variables ]

        public event EventHandler<AcceptEvtArgs> AcceptedEvtHandler;
        public event EventHandler<AcceptEvtArgs> ReceivedEvtHandler;

        private Socket m_lsnSocket;

        #endregion

        //===============================================//
        #region [ constructor and destructor ]

        public TcpServer()
        {
            m_lsnSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        ~TcpServer()
        {
            Dispose(false);
        }

        #endregion

        //===============================================//
        #region [ property ]

        public override Socket OwnedSocket
        {
            get { return m_lsnSocket; }
        }

        #endregion

        //===============================================//
        #region [ public methods ]

        public void Start(int port)
        {
            IPEndPoint lsnEndpoint = new IPEndPoint(IPAddress.Any, port);
            m_lsnSocket.Bind(lsnEndpoint);
            m_lsnSocket.Listen(10);

            m_lsnSocket.BeginAccept(OnAcceptedCallback, null);
        }

        #endregion

        //===============================================//
        #region [ private methods ]

        private void OnAcceptedCallback(IAsyncResult asyncResult)
        {
            Socket clientSocket = m_lsnSocket.EndAccept(asyncResult);
            EchoSocket echoSock = new EchoSocket(clientSocket);
            FireAcceptedEvent(echoSock);

            echoSock.Read();

            m_lsnSocket.BeginAccept(OnAcceptedCallback, null);
        }

        private void FireAcceptedEvent(EchoSocket acceptedSocket)
        {
            if (AcceptedEvtHandler != null)
                AcceptedEvtHandler(this, new AcceptEvtArgs(acceptedSocket));
        }

        #endregion
    }
}